"""
  The Event Router and Event Listener

  This file implements an event router scheme modeled after the
  one in Tekkotsu.

"""

from multiprocessing import Queue

from .trace import TRACE

#________________ Event base class ________________

class Event:
    """Base class for all events."""
    def __init__(self, source=None):
        self.source = source

    def __repr__(self):
        try:
            src_string = self.source.name
        except:
            src_string = repr(self.source)
        return '<%s from %s>' % (self.__class__.__name__, src_string)

#________________ Event Router ________________

class EventRouter:
    """An event router drives the state machine."""
    def __init__(self, robot):
        self.robot = robot
        # dispatch_table: event_class -> (source,listener)...
        self.dispatch_table = dict()
        # listener_registry: listener -> (event_class, source)...
        self.listener_registry = dict()
        # wildcard registry: true if listener is a wildcard (should run last)
        self.wildcard_registry = dict()
        # running processes
        self.processes = dict()  # id -> node
        self.interprocess_queue = Queue()

    def start(self):
        self.clear()
        self.poll_processes()

    def clear(self):
        self.dispatch_table.clear()
        self.listener_registry.clear()
        self.wildcard_registry.clear()
        self.processes.clear()
        self.interprocess_queue.close()
        self.interprocess_queue = Queue()

    def add_listener(self, listener, event_class, source):
        if not issubclass(event_class, Event):
            raise TypeError('% is not an Event' % event_type)
        source_dict = self.dispatch_table.get(event_class)
        if source_dict is None:
            source_dict = dict()
        handlers = source_dict.get(source, [])
        handlers.append(listener.handle_event)
        source_dict[source] = handlers
        self.dispatch_table[event_class] = source_dict
        reg_entry = self.listener_registry.get(listener,[])
        reg_entry.append((event_class,source))
        self.listener_registry[listener] = reg_entry

    # Transitions like =Hear('foo')=> must use None as the source
    # value because they do the matching themselves instead of relying
    # on the event router. So to distinguish a wildcard =Hear=>
    # transition, which must be invoked last, from all the other Hear
    # transitions, we must register it specially.
    def add_wildcard_listener(self, listener, event_class, source):
        self.add_listener(listener, event_class, source)
        self.wildcard_registry[listener.handle_event] = True

    def remove_listener(self, listener, event_class, source):
        try:
            del self.wildcard_registry[listener.handle_event]
        except: pass
        if not issubclass(event_class, Event):
            raise TypeError('% is not an Event' % event_class)
        source_dict = self.dispatch_table.get(event_class)
        if source_dict is None: return
        handlers = source_dict.get(source)
        if handlers is None: return
        try:
            handlers.remove(listener.handle_event)
        except: pass
        if handlers == []:
            del source_dict[source]
        if len(source_dict) == 0:   # no one listening for this event
            del self.dispatch_table[event_class]

    def remove_all_listener_entries(self, listener):
        for event_class, source in self.listener_registry.get(listener,[]):
            self.remove_listener(listener, event_class, source)
        try:
            del self.listener_registry[listener]
        except: pass

    def _get_listeners(self,event):
        source_dict = self.dispatch_table.get(type(event), None)
        if source_dict is None:  # no listeners for this event type
            return []
        source_matches = source_dict.get(event.source, [])
        match_handlers = []
        # TapEvent can be wildcarded even though the source is never None
        wildcard_matches = source_dict.get(None, []) if event.source is not None else []
        wildcard_handlers = []
        for handler in source_matches + wildcard_matches:
            if self.wildcard_registry.get(handler,False) is True:
                wildcard_handlers.append(handler)
            else:
                match_handlers.append(handler)
        # wildcard handlers must come last in the list
        return match_handlers + wildcard_handlers

    def post(self,event):
        if not isinstance(event,Event):
            raise TypeError('%s is not an Event' % event)
        listeners = self._get_listeners(event)
        cnt = 0
        for listener in listeners:
            cnt += 1
            if TRACE.trace_level >= TRACE.listener_invocation:
                print('TRACE%d:' % TRACE.listener_invocation, listener.__self__, 'receiving', event)
            self.robot.loop.call_soon_threadsafe(listener,event)
    
    def add_process_node(self, node):
        self.processes[id(node)] = node

    def delete_process_node(self, node):
        node_id = id(node)
        if node_id in self.processes:
            del self.processes[node_id]
            # print('Deleted id',node_id,'for',node)
        else:
            print('*** ERROR in delete_process_node: node',node_id,'not in process dict!')

    POLLING_INTERVAL = 0.1

    def poll_processes(self):
        while not self.interprocess_queue.empty():
            (id,event) = self.interprocess_queue.get()
            if id in self.processes:
                node = self.processes[id]
                event.source = node
                print('Node %s returned %s' % (node,event))
                self.post(event)
            else:
                print('*** ERROR in poll_processes: node',id,'not in process dict!', self.processes)
        self.robot.loop.call_later(self.POLLING_INTERVAL, self.poll_processes)

#________________ Event Listener ________________

class EventListener:
    """Parent class for both StateNode and Transition."""
    def __init__(self):
        global robot_for_loading
        rep = object.__repr__(self)
        self.name = rep[1+rep.rfind(' '):-1]  # name defaults to hex address
        self.running = False
        if not hasattr(self,'polling_interval'):
            self.polling_interval = None
        self.poll_handle = None
        self._robot = robot_for_loading

    def __repr__(self):
        return '<%s %s>' % (self.__class__.__name__, self.name)

    def set_name(self,name):
        if not isinstance(name,str):
            raise ValueError('name must be a string, not %s' % name)
        self.name = name
        return self

    def start(self):
        self.running = True
        if self.polling_interval:
            self.poll_handle = \
                self.robot.loop.call_later(self.polling_interval, self._next_poll)
        else:
            self.poll_handle = None

    def stop(self):
        if not self.running: return
        self.running = False
        if self.poll_handle:
            self.poll_handle.cancel()
            self.poll_handle = None
        self.robot.erouter.remove_all_listener_entries(self)

    def handle_event(self, event):
        pass

    def set_polling_interval(self,interval):
        if interval is None or isinstance(interval, (int,float)):
            self.polling_interval = interval
        else:
            raise TypeError('interval must be a number')
        if self.poll_handle:
            self.poll_handle.cancel()
        if interval is None:
            self.poll_handle = None
        else:
            self.poll_handle = \
                self.robot.loop.call_later(self.polling_interval, self._next_poll)
            if TRACE.trace_level >= TRACE.polling:
                print('TRACE%d: set polling interval' % TRACE.polling, self.polling_interval)

    def _next_poll(self):
        """Called to schedule the next polling interval and then poll the node."""
        # Schedule the next poll first because self.poll may cancel it.
        if self.running and self.polling_interval:
            self.poll_handle = \
                self.robot.loop.call_later(self.polling_interval, self._next_poll)
            self.poll()

    def poll(self):
        """Dummy polling function in case subclass neglects to supply one."""
        if TRACE.trace_level >= TRACE.polling:
            print('TRACE%d: polling' % TRACE.polling, self)
        print('%s has no poll() method' % self)
